package com.sixbro.shiro.jwt.config.shiro;

import com.bxs.rbac.shiro.jwt.constant.Constant;
import com.bxs.rbac.shiro.jwt.domain.entity.Authority;
import com.bxs.rbac.shiro.jwt.domain.entity.User;
import com.bxs.rbac.shiro.jwt.service.token.TokenService;
import com.bxs.rbac.shiro.jwt.service.user.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 身份校验核心类
 * Realm是一个Dao，通过它来验证用户身份和权限。这里Shiro不做权限的管理工作，需要我们自己管理用户权限，只需要从我们的数据源中把用户和用户的角色权限信息取出来交给Shiro即可。Shiro就会自动的进行权限判断。
 * </p>
 *
 * @Author: Mr.Lu
 * @Since: 2020/6/17 17:02
 */
public class ShiroRealm extends AuthorizingRealm {
    private static Logger _logger = LoggerFactory.getLogger(ShiroRealm.class);

    /**
     * 这里尝试过使用@Autowired 但是发现会报错。这个是spring的注解。如果有知道原因的可以留言。谢谢
     */
    @Autowired
    private TokenService tokenService;
    @Autowired
    private UserService userService;
    @Resource
    private RedisTemplate template;


    /**
     * 必须重写此方法，不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }


    /**
     * 配置权限　注入权限
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        _logger.info("------------ 权限配置 ----------");

        User user = (User)principals.getPrimaryPrincipal();
        String key = Constant.REDIS_AUTHC_USER_INFO + user.getId();

        if (template.hasKey(key)) {
            user = (User) template.opsForValue().get(key);
        } else {
            // 注入角色所有权限（查询用户所有的权限注入控制器）
            List<Authority> authorities = userService.findAuthoritiesByUserId(user.getId());
            if(!CollectionUtils.isEmpty( authorities )) {
                user.setAuthorities( authorities );
            }

            //redis 缓存用户信息
            template.opsForValue().set( key, user, 1, TimeUnit.DAYS);
        }

        if (CollectionUtils.isEmpty( user.getAuthorities() )) {
            return null;
        }

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        for (Authority authority : user.getAuthorities()) {
            authorizationInfo.addStringPermission( authority.getEname() );
        }

        return authorizationInfo;
    }


    /**
     * 用户验证
     * @param authenticationToken 账户数据
     * @return
     * @throws AuthenticationException 根据账户数据查询账户。根据账户状态抛出对应的异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        _logger.info("----------- 用户认证 ----------");

        String token = ( String ) authenticationToken.getCredentials();
        User user = null;
        // 解密获得username，用于和数据库进行对比
        try {
            user = tokenService.getUser(token);
        } catch (Exception e) {
            throw new AuthenticationException("token invalid");
        }

        //这里需注意。看别人的教程有人是这样写的String password = (String) token.getCredentials();
        //项目运行的时候报错，发现密码不正确。后来进源码查看发现将密码注入后。Shiro会进行转义将字符串转换成字符数组。
        //源码：this(username, password != null ? password.toCharArray() : null, false, null);
        //不晓得是否是因为版本的原因，建议使用的时候下载源码进行查看
        //String password = new String((char[]) token.getCredentials());
        //通过username从数据库中查找 User对象，如果找到，没找到.
        //实际项目中，这里可以根据实际情况做缓存，如果不做，Shiro自己也是有时间间隔机制，2分钟内不会重复执行该方法

        String password = user.getPassword();
        String key = Constant.REDIS_AUTHC_USER_INFO + user.getId();

        if (template.hasKey(key)) {
            user = (User) template.opsForValue().get(key);
        } else {
            user = userService.findUserByUsername( user.getUsername() );
        }

        if(null == user){
            throw new AuthenticationException("用户不存在!");
        }

        if( !tokenService.verify( token, user ) ){
            throw new AuthenticationException("Token认证失败");
        }

        // 状态:  1-正常 2-锁定  3-禁用 4-异常
        switch ( user.getStatus() ) {
            case LOCKED:
                throw new LockedAccountException();
            case Disabled:
                throw new DisabledAccountException();
            default:
        }

        SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(token, token, getName());
        return authorizationInfo;
    }
}
